using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Dhpoware;

namespace _3D_Game
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        /// <summary>
        /// A material. This material structure is the same as the one defined
        /// in the parallax_normal_mapping.fx file. We use the Color type here
        /// instead of a four element floating point array.
        /// </summary>
        private struct Material
        {
            public Color Ambient;
            public Color Diffuse;
            public Color Emissive;
            public Color Specular;
            public float Shininess;
        }

        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        // Model stuff
        ModelManager modelManager;

        // Camera
        public Camera camera { get; protected set; }

        //public CameraComponent camera;
        // Random
        public Random rnd { get; protected set; }

        // Shot variables
        float shotSpeed = 200;
        int shotDelay = 300;
        int shotCountdown = 0;

        // Crosshair
        Texture2D crosshairTexture;

        // Audio
        AudioEngine audioEngine;
        WaveBank waveBank;
        SoundBank soundBank;
        Cue trackCue;

        // Game state items
        public enum GameState { START, PLAY, LEVEL_CHANGE, END }
        GameState currentGameState = GameState.START;

        // Splash screen and scoring
        SplashScreen splashScreen;
        int score = 0;
        SpriteFont scoreFont;

        // Powerup stuff
        int originalShotDelay = 300;
        public enum PowerUps { RAPID_FIRE }
        int shotDelayRapidFire = 100;
        int rapidFireTime = 10000;
        int powerUpCountdown = 0;
        string powerUpText = "";
        int powerUpTextTimer = 0;
        SpriteFont powerUpFont;

        KeyboardState lastKeyboardState = new KeyboardState();
        KeyboardState currentKeyboardState = new KeyboardState();
        Ship ship = new Ship();

        Model shipModel;
        Model groundModel;
        Model terrain;
        Vector3 terrainPosition;
        Matrix terrainWorld;
        bool cameraSpringEnabled = true;

        float aspectRatio;
        //floor map staff
        private const float FLOOR_WIDTH = 80000.0f;
        private const float FLOOR_HEIGHT = 80000.0f;
        private const float FLOOR_TILE_U = 80000.0f;
        private const float FLOOR_TILE_V = 80000.0f;
        private Texture2D nullTexture;
        private Texture2D floorColorMap;
        private Texture2D floorNormalMap;
        private Texture2D floorHeightMap;
        private VertexBuffer floorVertexBuffer;
        //material
        private Material material;
        private Effect mapEffect;
        private bool disableColorMap = true;
        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            rnd = new Random();

            // Set preferred resolution
            graphics.PreferredBackBufferWidth = 1280;
            graphics.PreferredBackBufferHeight = 1024;

// If not running debug, run in full screen
#if !DEBUG
    graphics.IsFullScreen = true;
#endif
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            // Initialize Camera
            camera = new Camera(this, new Vector3(0.0f, 0.0f, 50.0f),
                Vector3.Zero, Vector3.Up);

            //camera.NearPlaneDistance = 10;
            //camera.FarPlaneDistance = 10000;
            Components.Add(camera);
            // Set the camera offsets
            //camera.DesiredPositionOffset = new Vector3(0.0f, 2.0f, 2.0f);
            //camera.LookAtOffset = new Vector3(0.0f, 2.8f, 0.0f);

            // Set camera perspective


            camera.AspectRatio = (float)graphics.GraphicsDevice.Viewport.Width /
            graphics.GraphicsDevice.Viewport.Height;

            //camera.AspectRatio = 4.0f/3.0f;
            // Perform an inital reset on the camera so that it starts at the resting
            // position. If we don't do this, the camera will start at the origin and
            // race across the world to get behind the chased object.
            // This is performed here because the aspect ratio is needed by Reset.
            UpdateCameraChaseTarget();
            camera.Reset();
            // Initialize model manager
            modelManager = new ModelManager(this);
            Components.Add(modelManager);
            modelManager.Enabled = false;
            modelManager.Visible = false;

            splashScreen = new SplashScreen(this);
            Components.Add(splashScreen);
            splashScreen.SetData("Welcome to Space Defender!",
                currentGameState);
            
            InitFloor();

            // Initialize material settings for the floor.
            material.Ambient = new Color(new Vector4(0.2f, 0.2f, 0.2f, 1.0f));
            material.Diffuse = new Color(new Vector4(0.8f, 0.8f, 0.8f, 1.0f));
            material.Emissive = Color.Black;
            material.Specular = Color.White;
            material.Shininess = 90.0f;
            base.Initialize();
        }

        private void InitFloor()
        {
            NormalMappedQuad floor = new NormalMappedQuad(
                Vector3.Zero, Vector3.Up, Vector3.Forward,
                FLOOR_WIDTH, FLOOR_HEIGHT, FLOOR_TILE_U, FLOOR_TILE_V);

            floorVertexBuffer = new VertexBuffer(GraphicsDevice,
                typeof(NormalMappedVertex), floor.Vertices.Length,
                BufferUsage.WriteOnly);

            floorVertexBuffer.SetData(floor.Vertices);
        }
        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // Load the crosshair texture
            crosshairTexture = Content.Load<Texture2D>(@"textures\crosshair");

            // Load sounds and play initial sounds
            audioEngine = new AudioEngine(@"Content\Audio\GameAudio.xgs");
            waveBank = new WaveBank(audioEngine, @"Content\Audio\Wave Bank.xwb");
            soundBank = new SoundBank(audioEngine, @"Content\Audio\Sound Bank.xsb");
            
            // Play the soundtrack
            trackCue = soundBank.GetCue("Tracks");
            trackCue.Play();

            // Load fonts
            scoreFont = Content.Load<SpriteFont>(@"Fonts\ScoreFont");
            powerUpFont = Content.Load<SpriteFont>(@"fonts\PowerupFont");

            shipModel = Content.Load<Model>(@"Models\ship");
            //terrain = Content.Load<Model>("Models\\terrain");
            //terrainPosition = Vector3.Zero;
            //groundModel = Content.Load<Model>(@"Models\ground");
            //aspectRatio = graphics.GraphicsDevice.Viewport.AspectRatio;
            mapEffect = Content.Load<Effect>(@"Effects\parallax_normal_mapping");

            floorColorMap = Content.Load<Texture2D>(@"Textures\floor_color_map");
            floorNormalMap = Content.Load<Texture2D>(@"Textures\floor_normal_map");
            floorHeightMap = Content.Load<Texture2D>(@"Textures\floor_height_map");

            // Create an empty white texture. This will be bound to the
            // colorMapTexture shader parameter when the user wants to
            // disable the color map texture. This trick will allow the
            // same shader to be used for when textures are enabled and
            // disabled.

            nullTexture = new Texture2D(GraphicsDevice, 1, 1, false, SurfaceFormat.Color);

            Color[] pixels = { Color.White };

            nullTexture.SetData(pixels);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            lastKeyboardState = currentKeyboardState;
            //lastGamePadState = currentGamePadState;

            currentKeyboardState = Keyboard.GetState();
            //currentGamePadState = GamePad.GetState(PlayerIndex.One);

            // Allows the game to exit
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // Only check for shots if you're in the play game state
            if (currentGameState == GameState.PLAY)
            {
                // See if the player has fired a shot
                FireShots(gameTime);
            }
            modelManager.CollidesWith(shipModel, ship.World);
            // Update power-up timer
            UpdatePowerUp(gameTime);

            // Pressing the A button or key toggles the spring behavior on and off
            if (lastKeyboardState.IsKeyUp(Keys.A) &&
                (currentKeyboardState.IsKeyDown(Keys.A)))
            {
                cameraSpringEnabled = !cameraSpringEnabled;
            }

            // Reset the ship on R key or right thumb stick clicked
            if (currentKeyboardState.IsKeyDown(Keys.R))
            {
                ship.Reset();
                camera.Reset();
            }

            // Update the ship
            ship.Update(gameTime);

            // Update the camera to chase the new target
            UpdateCameraChaseTarget();
            UpdateEffect();
            // The chase camera's update behavior is the springs, but we can
            // use the Reset method to have a locked, spring-less camera
            if (cameraSpringEnabled)
                camera.Update(gameTime);
            else
                camera.Reset();
            base.Update(gameTime);
        }
        private void UpdateEffect()
        {

            // Set the shader material parameters.
            mapEffect.Parameters["material"].StructureMembers["ambient"].SetValue(material.Ambient.ToVector4());
            mapEffect.Parameters["material"].StructureMembers["diffuse"].SetValue(material.Diffuse.ToVector4());
            mapEffect.Parameters["material"].StructureMembers["emissive"].SetValue(material.Emissive.ToVector4());
            mapEffect.Parameters["material"].StructureMembers["specular"].SetValue(material.Specular.ToVector4());
            mapEffect.Parameters["material"].StructureMembers["shininess"].SetValue(material.Shininess);

            // Bind the texture maps to the shader.
            mapEffect.Parameters["colorMapTexture"].SetValue((disableColorMap) ? nullTexture : floorColorMap);
            mapEffect.Parameters["normalMapTexture"].SetValue(floorNormalMap);
            mapEffect.Parameters["heightMapTexture"].SetValue(floorHeightMap);
        }

        /// <summary>
        /// Update the values to be chased by the camera
        /// </summary>
        private void UpdateCameraChaseTarget()
        {
            camera.cameraPosition = ship.Position;
            camera.cameraDirection = ship.Direction;
            camera.cameraUp = ship.Up;
        }
        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            DrawModel(shipModel, ship.World);
            DrawFloor();
            //terrainWorld = Matrix.CreateTranslation(terrainPosition);
            //DrawModel(terrain, terrainWorld);
            base.Draw(gameTime);

            // Only draw crosshair if in play game state
            if (currentGameState == GameState.PLAY)
            {

                // Draw the crosshair
                spriteBatch.Begin();

                spriteBatch.Draw(crosshairTexture,
                    new Vector2((Window.ClientBounds.Width / 2)
                        - (crosshairTexture.Width / 2),
                        (Window.ClientBounds.Height / 2)
                        - (crosshairTexture.Height / 2)),
                        Color.White);

                // Draw the current score
                string scoreText = "Score: " + score;
                spriteBatch.DrawString(scoreFont, scoreText,
                    new Vector2(10, 10), Color.Red);

                // Let the player know how many misses he has left
                spriteBatch.DrawString(scoreFont, "Misses Left: " +
                    modelManager.missesLeft,
                    new Vector2(10, scoreFont.MeasureString(scoreText).Y + 20),
                    Color.Red);

                // If power-up text timer is live, draw power-up text
                if (powerUpTextTimer > 0)
                {
                    powerUpTextTimer -= gameTime.ElapsedGameTime.Milliseconds;
                    Vector2 textSize = powerUpFont.MeasureString(powerUpText);
                    spriteBatch.DrawString(powerUpFont,
                        powerUpText,
                        new Vector2((Window.ClientBounds.Width / 2) -
                        (textSize.X / 2),
                        (Window.ClientBounds.Height / 2) -
                        (textSize.Y / 2)),
                        Color.Goldenrod);
                }

                spriteBatch.End();
            }
        }

        private void DrawFloor()
        {
            GraphicsDevice.SetVertexBuffer(floorVertexBuffer);

            foreach (EffectPass pass in mapEffect.CurrentTechnique.Passes)
            {
                pass.Apply();
                GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 2);
            }
        }

        /// <summary>
        /// Simple model drawing method. The interesting part here is that
        /// the view and projection matrices are taken from the camera object.
        /// </summary>        
        private void DrawModel(Model model, Matrix world)
        {
            Matrix[] transforms = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(transforms);

            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;
                    effect.World = transforms[mesh.ParentBone.Index] * world;

                    // Use the matrices provided by the chase camera
                    effect.View = camera.view;
                    effect.Projection = camera.projection;
                }
                mesh.Draw();
            }
        }

        protected void FireShots(GameTime gameTime)
        {
            if (shotCountdown <= 0)
            {
                // Did player press space bar or left mouse button?
                if (Keyboard.GetState().IsKeyDown(Keys.Space) ||
                    Mouse.GetState().LeftButton == ButtonState.Pressed)
                {
                    // Add a shot to the model manager
                    modelManager.AddShot(
                        camera.cameraPosition + new Vector3(0, -5, 0),
                        camera.GetCameraDirection * shotSpeed);

                    // Play shot audio
                    PlayCue("Shot");

                    // Reset the shot countdown
                    shotCountdown = shotDelay;
                }
            }
            else
                shotCountdown -= gameTime.ElapsedGameTime.Milliseconds;
        }

        public void PlayCue(string cue)
        {
            soundBank.PlayCue(cue);
        }

        public void ChangeGameState(GameState state, int level)
        {
            CancelPowerUps();

            currentGameState = state;

            switch (currentGameState)
            {
                case GameState.LEVEL_CHANGE:
                    splashScreen.SetData("Level " + (level + 1),
                        GameState.LEVEL_CHANGE);
                    modelManager.Enabled = false;
                    modelManager.Visible = false;
                    splashScreen.Enabled = true;
                    splashScreen.Visible = true;

                    // Stop the soundtrack loop
                    trackCue.Stop(AudioStopOptions.Immediate);
                    break;

                case GameState.PLAY:
                    modelManager.Enabled = true;
                    modelManager.Visible = true;
                    splashScreen.Enabled = false;
                    splashScreen.Visible = false;

                    if (trackCue.IsPlaying)
                        trackCue.Stop(AudioStopOptions.Immediate);

                    // To play a stopped cue, get the cue from the soundbank again
                    trackCue = soundBank.GetCue("Tracks");
                    trackCue.Play();
                    break;

                case GameState.END:
                    splashScreen.SetData("Game Over.\nLevel: " + (level + 1) +
                        "\nScore: " + score, GameState.END);
                    modelManager.Enabled = false;
                    modelManager.Visible = false;
                    splashScreen.Enabled = true;
                    splashScreen.Visible = true;

                    // Stop the soundtrack loop
                    trackCue.Stop(AudioStopOptions.Immediate);
                    break;
            }
        }

        public void AddPoints(int points)
        {
            score += points;
        }

        private void CancelPowerUps()
        {
            modelManager.consecutiveKills = 0;
            shotDelay = originalShotDelay;
        }

        protected void UpdatePowerUp(GameTime gameTime)
        {
            if (powerUpCountdown > 0)
            {
                powerUpCountdown -= gameTime.ElapsedGameTime.Milliseconds;
                if (powerUpCountdown <= 0)
                {
                    CancelPowerUps();
                    powerUpCountdown = 0;
                }
            }
        }

        public void StartPowerUp(PowerUps powerUp)
        {
            switch (powerUp)
            {
                case PowerUps.RAPID_FIRE:
                    shotDelay = shotDelayRapidFire;
                    powerUpCountdown = rapidFireTime;
                    powerUpText = "Rapid Fire Mode!";
                    powerUpTextTimer = 1000;
                    soundBank.PlayCue("RapidFire");
                    break;
            }
        }
    }
}
